/* XMPP/jabber related routines.
 *
 * Copyright (c) 2008 Cian Synnott.
 * 
 * $Id: xmpp.c 28 2009-12-04 03:01:17Z cian $
 */
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/queue.h>
#include <unistd.h>

#include <event.h>
#include <iksemel.h>

#include "time_format.h"
#include "log.h"
#include "utility.h"
#include "write_self.h"
#include "xmpp.h"

struct _xmpp_session {
  struct event *evt;
  iksparser *prs;
  iksid *acc;
  iksfilter *filter;
  char *password;
  int authorized;
  int features;

  int status;
  char *statusmessage;

  xmpp_status *statuses;
  xmpp_user_message *messages;
};

/* Handle an auth result.
 *
 * Type iksFilterHook.
 *
 * Called back by xmpp_session filter when an auth result is received.
 *
 * This is essentially the start of our real xmpp_session, so we send presence.
 */
static int cb_on_auth_result(xmpp_session *sess, ikspak *pak) {
  xmpp_send_status(sess);
  return IKS_FILTER_EAT;
}

/* Handle an incoming message.
 *
 * Type iksFilterHook.
 *
 * Called back by xmpp_session filter when a message is received.
 */
static int cb_on_message(xmpp_session *sess, ikspak *pak) {
  int buffer_size; 
  char *buffer, *timestamp;
  char *message = iks_find_cdata(pak->x, "body");

  buffer_size = strlen(message) + strlen(pak->from->partial) + 128;
  buffer = calloc(buffer_size, sizeof(char));
  if (buffer == NULL)
    log_errx(EXIT_FAILURE, "failed to allocate memory for message formatting");

  timestamp = time_formatted_by("%H:%M:%S", 0);
  if (timestamp == NULL)
    log_errx(EXIT_FAILURE, "failed to create timestamp for message formatting");

  if (message == NULL) {
    log_err("Got a NULL message body from '%s'", pak->from->partial);
    return IKS_FILTER_PASS;
  }

  snprintf(buffer, buffer_size, "\nJabber message from %s at %s ...\n%s\nEOM\n",
    pak->from->partial, timestamp, message);

  write_self(buffer);

  free(buffer);
  free(timestamp);

  return IKS_FILTER_EAT;
}

/* Update the session's list of jid statuses.
 *
 * This procedure lets us know whether it made any changes, and is a little
 * long and mucky as a result.
 *
 * Args:
 * - session: an xmpp_session
 * - jid: jid of the update.
 * - show: 'show' status of the update.
 * - message: status message of the update.
 * Returns:
 * - True if the status update resulted in a change; false otherwise.
 */
static int update_statuses(xmpp_session *sess, char *jid, char *show,
  char *message)
{
  xmpp_status *status = sess->statuses;
  int changed = 0;
  int found = 0;

  while (status) {
    if (!strcmp(status->jid, jid)) {
      if (strcmp(show, status->show))
        changed = 1;

      if (((status->message != NULL) && (message == NULL)) ||
          ((status->message == NULL) && (message != NULL)))
        changed = 1;

      if ((status->message != NULL) && (message != NULL))
        if (strcmp(status->message, message))
          changed = 1;

      found = 1;
      break;
    }
    status = status->next;
  }
  if (!found) {
    /* TODO(cian): If we ever find ourselves caring, insert sorted here. */
    if((status = calloc(1, sizeof(xmpp_status))) == NULL) {
      log_err("Error in calloc() for new status for jid %s: %s", jid,
        strerror(errno));
      return 0;
    }
    status->next = sess->statuses;
    sess->statuses = status;
    status->jid = strdup(jid);
    changed = 1;
  }

  if (!changed)
    return 0;

  if (status->show)
    free(status->show);
  status->show = strdup(show);

  if (status->message)
    free(status->message);

  if (message)
    status->message = strdup(message); 
  else
    status->message = NULL;

  return 1;
}

/* Update the session's list of user messages.
 *
 * These are messages about status changes to be delivered to the user.
 *
 * Args:
 * - session: an xmpp_session
 * - jid: jid of the update.
 * - show: 'show' status of the update.
 * - message: status message of the update.
 */
#define MESSAGE_SIZE 1024
static void update_messages(xmpp_session *sess, char *jid, char *show,
  char *message)
{
  xmpp_user_message *current, *tail;
  char *buffer;
  int written;

  if ((buffer = malloc(MESSAGE_SIZE)) == NULL) {
    log_err("Error in malloc() for message buffer: %s", strerror(errno));
    return;
  }

  if (message)
    written = snprintf(buffer, MESSAGE_SIZE, "%s is %s: %s", jid, show, message);
  else
    written = snprintf(buffer, MESSAGE_SIZE, "%s is %s.", jid, show);
  if (written < 0 || written >= MESSAGE_SIZE) {
    log_err("Error constructing a message string for "
      "jid <%s>, show <%d>, message <%s>.", jid, show,
      message ? message : "");
    free(buffer);
    return;
  }

  if((current = calloc(1, sizeof(xmpp_user_message))) == NULL) {
    log_err("Error in calloc() for new message for jid %s: %s", jid,
      strerror(errno));
    free(buffer);
    return;
  }
  current->message = buffer;

  if (sess->messages) {
    for (tail = sess->messages; tail->next; tail = tail->next);
    tail->next = current;
  }
  else {
    sess->messages = current;
  }
  current->next = NULL;
}

/* Handle an incoming presence.
 *
 * Type iksFilterHook.
 *
 * Called back by xmpp_session filter when presence is received.
 */
static int cb_on_presence(xmpp_session *sess, ikspak *pak) {
  char *show;
  int changed;

  switch(pak->show) {
    case IKS_SHOW_UNAVAILABLE:
      show = "unavailable";
      break;
    case IKS_SHOW_AVAILABLE:
      show = "available";
      break;
    case IKS_SHOW_CHAT:
      show = "free";
      break;
    case IKS_SHOW_AWAY:
      show = "away";
      break;
    case IKS_SHOW_XA:
      show = "away";
      break;
    case IKS_SHOW_DND:
      show = "busy";
      break;
    default:
      show = "unknown";
  }

  changed = update_statuses(sess, pak->from->partial, show,
    iks_find_cdata(pak->x, "status"));

  if (changed)
    update_messages(sess, pak->from->partial, show,
      iks_find_cdata(pak->x, "status"));

  return IKS_FILTER_EAT;
}

/* Log a message.
 *
 * Type iksLogHook.
 *
 * Called back whenever a message is sent or received.
 *
 * This gives us a full dump of traffic to and from the server for debugging.
 */
static void cb_on_log(xmpp_session *sess, const char *text, size_t size,
    int is_incoming) {
  char *type = "RECV";
  char *secure = "";
  if (iks_is_secure (sess->prs)) secure = "Sec";
  if (!is_incoming) type = "SEND";
  log_debug("%s%s [%s]", secure, type, text);
}

/* Handle xmpp streams.
 *
 * Type iksStreamHook.
 *
 * Called back whenever stream control is received.
 *
 * Handles connection and authorization of our session.
 */
static int cb_on_stream(xmpp_session *sess, int type, iks *node) {
  switch(type) {
    case IKS_NODE_START:
      if (!iks_is_secure(sess->prs))
        iks_start_tls(sess->prs);
      break;
    case IKS_NODE_NORMAL:
      if (strcmp("stream:features", iks_name(node)) == 0) {
        sess->features = iks_stream_features(node);
        if (!iks_is_secure(sess->prs)) break;
        if (sess->authorized) {
          iks *t;
          if (sess->features & IKS_STREAM_BIND) {
            t = iks_make_resource_bind(sess->acc);
            iks_send(sess->prs, t);
            iks_delete (t);
          }
          if (sess->features & IKS_STREAM_SESSION) {
            t = iks_make_session();
            iks_insert_attrib(t, "id", "auth");
            iks_send(sess->prs, t);
            iks_delete (t);
          }
        } else {
          if (sess->features & IKS_STREAM_SASL_MD5)
            iks_start_sasl(sess->prs, IKS_SASL_DIGEST_MD5, sess->acc->user,
              sess->password);
          else if (sess->features & IKS_STREAM_SASL_PLAIN)
            iks_start_sasl(sess->prs, IKS_SASL_PLAIN, sess->acc->user,
              sess->password);
        }
      }
      else if (strcmp("success", iks_name(node)) == 0) {
        sess->authorized = 1;
        log_info("Authorized as %s", sess->acc->partial);
        iks_send_header(sess->prs, sess->acc->server);
      }
      else if (strcmp("failure", iks_name(node)) == 0)
        log_errx(EXIT_FAILURE, "SASL authentication failed");
      else {
        ikspak *pak;

        pak = iks_packet(node);
        iks_filter_packet(sess->filter, pak);
      }
      break;
    case IKS_NODE_STOP:
      log_errx(EXIT_FAILURE, "Server disconnected");
    case IKS_NODE_ERROR:
      log_errx(EXIT_FAILURE, "Stream error");
    default:
      log_errx(EXIT_FAILURE, "Unknown node type %d", type);
  }
  
  if (node) iks_delete(node);
  return IKS_OK;
}

/* Callback for processing data whenever we read it from our xmpp connection.
 *
 * Called back from libevent's dispatch loop.
 *
 * Args:
 * - fd: file descriptor the data has come in on
 * - event: for this, only ever EV_READ
 * - arg: user-defined data passed back to us; in this case, a xmpp_session
 */
static void xmpp_process_data(int fd, short event, void *arg) {
  xmpp_session *sess = arg;
  int result;

  result = iks_nonblocking_recv(sess->prs, 0);
  if (result == IKS_HOOK)
    /* TODO(cian): Understand IKS_HOOK, etc. */
    log_errx(EXIT_FAILURE, "IKS_HOOK received, exiting");
  else if (result == IKS_NET_TLSFAIL)
    log_errx(EXIT_FAILURE, "TLS setup failed");
  else if (result != IKS_OK)  
    log_errx(EXIT_FAILURE, "IKS parser failure");
}

/* See xmpp.h */
xmpp_session *xmpp_setup(char *jid, char *password, char *server, int port,
  char *status)
{
  xmpp_session *sess;
  int result;

  sess = calloc(1, sizeof(xmpp_session));
  if (sess == NULL)
    log_errx(EXIT_FAILURE, "calloc() failed allocating a new xmpp_session: %s", strerror(errno));

  sess->evt = calloc(1, sizeof(struct event));
  if (sess->evt == NULL)
    log_errx(EXIT_FAILURE, "calloc() failed allocating a new struct event: %s", strerror(errno));

  sess->prs = iks_stream_new(IKS_NS_CLIENT, sess,
    (iksStreamHook *) cb_on_stream);
  sess->acc = iks_id_new(iks_parser_stack(sess->prs), jid);

  /* No resource supplied, make a default */
  if (sess->acc->resource == NULL) {
    char *tmp = iks_malloc(strlen(sess->acc->user) + strlen(sess->acc->server)
      + strlen("ohai") + 3);
    sprintf (tmp, "%s@%s/%s", sess->acc->user, sess->acc->server, "ohai");
    sess->acc = iks_id_new(iks_parser_stack(sess->prs), tmp);
    iks_free(tmp);
  }

  iks_set_log_hook(sess->prs, (iksLogHook *) cb_on_log);

  sess->password = password;

  sess->filter = iks_filter_new();
  iks_filter_add_rule(sess->filter, (iksFilterHook *) cb_on_auth_result, sess,
    IKS_RULE_TYPE, IKS_PAK_IQ, IKS_RULE_SUBTYPE, IKS_TYPE_RESULT,
    IKS_RULE_ID, "auth", IKS_RULE_DONE);
  iks_filter_add_rule(sess->filter, (iksFilterHook *) cb_on_message, sess,
    IKS_RULE_TYPE, IKS_PAK_MESSAGE, IKS_RULE_DONE);
  iks_filter_add_rule(sess->filter, (iksFilterHook *) cb_on_presence, sess,
    IKS_RULE_TYPE, IKS_PAK_PRESENCE, IKS_RULE_DONE);

  if (server == NULL) server = sess->acc->server;
  if (port == 0) port = IKS_JABBER_PORT;
  result = iks_connect_with(sess->prs, server, port, sess->acc->server,
    &iks_default_transport);
  switch(result) {
    case IKS_OK:
      break;
    case IKS_NET_NODNS:
      log_errx(EXIT_FAILURE, "DNS lookup of '%s' failed", server);
    case IKS_NET_NOCONN:
      log_errx(EXIT_FAILURE, "Connection to '%s' failed", server);
    default:
      log_errx(EXIT_FAILURE, "Unspecified error %d from iksemel", result);
  }

  log_info("Connected to %s", server);
  if (set_nonblocking(iks_fd(sess->prs)) < 0) 
    log_errx(EXIT_FAILURE, "Failed to set socket nonblocking: %s",
      strerror(errno));
  log_info("Set socket nonblocking");

  if (status == NULL)
    status = "ohai!";
  xmpp_set_status(sess, 1, status);

  sess->statuses = NULL;
  sess->messages = NULL;

  event_set(sess->evt, iks_fd(sess->prs), EV_READ | EV_PERSIST,
    xmpp_process_data, sess);
  event_add(sess->evt, NULL);

  return sess;
}

/* See xmpp.h */
void xmpp_set_status(xmpp_session *sess, int status, char *message) {
  sess->status = status;
  free(sess->statusmessage);
  sess->statusmessage = strdup(message);
}

/* See xmpp.h */
void xmpp_send_status(xmpp_session *sess) {
  iks *pres;
  int available;

  if (sess->statusmessage == NULL) return;

  available = sess->status ? IKS_SHOW_AVAILABLE : IKS_SHOW_UNAVAILABLE; 

  pres = iks_make_pres(available, sess->statusmessage); 
  iks_send(sess->prs, pres);
  iks_delete(pres);
}

/* See xmpp.h */
xmpp_status *xmpp_get_contact_status(xmpp_session *sess) {
  return sess->statuses;
}

/* See xmpp.h */
xmpp_user_message *xmpp_get_user_messages(xmpp_session *sess) {
  xmpp_user_message *messages = sess->messages;
  sess->messages = NULL;
  return messages;
}

/* See xmpp.h */
void xmpp_send_message(xmpp_session *sess, char *to, char *message) {
  iks *msg;
  msg = iks_make_msg(IKS_TYPE_CHAT, to, message);
  iks_send(sess->prs, msg);
  iks_delete(msg);
}

/* See xmpp.h */
void xmpp_teardown(xmpp_session *sess) {
  if (event_del(sess->evt) == -1)
    log_err("event_del failed for xmpp session event");
  free(sess->evt);
  iks_parser_delete(sess->prs);
  iks_filter_delete(sess->filter);
  if (sess->statusmessage)
    free(sess->statusmessage);
  free(sess);
}
